
'use strict';

/**
	Part of Zombie Navigator
	Copyright © 2015  Zombie Navigator Developers
	
	@file
	@module
	@license AGPL-3.0+
*/


/* Used modules */
const _modSerializeFunction = require('./serialize-function');


/* Private properties */

/**
	@private
*/
const _toJSON = JSON.stringify.bind(JSON);

/**
	@private
*/
const _bindFunction = Object.getPrototypeOf(function () {}).bind;


/* Exported APIs */

/**
	Compose the content script from a callback in a secure way.
	The use of contentScriptFile is not relevant here since we use the callback
	supplied by the Zombie script in run time. (And it is the user that
	provides a Zombie script, which is a single file.)
	
	If we gave the script unlimited access to plain `contentScriptFile`, it
	can mess up the API with that privilege. So it is more smart to build the
	`contentScript` this way to restrict access to the internal communication
	chunnels.
	
	See the 'main' demo for a usage example.
	@public
*/
const buildScript = (callback, args) => {
	const modSerializeFunction = _modSerializeFunction;
	
	if ('function' != typeof callback) {
		throw new TypeError('Not a function');
	}
	
	//callback = _bindFunction.call(callback, null); // callback.bind(null)
	
	
	/*
		Use the protected toString() function to serialize the callback
		The resulting callback is always a valid function expression, which
		cannot be fooled.
	*/
	const funcStr = modSerializeFunction.serializeFunction(callback);
	
	
	/*
		JSON is not a strict subset of ECMAScript
		U+2028 and U+2029 is not allowed in ECMAScript but in JSON allowed
		inside strings. Fortunately, simple replacement is completely valid
		here. Reference: ECMAScript 2015, the 6th Edition.
		
		See: https://github.com/rack/rack-contrib/pull/37
		
		This is an error:
		eval(JSON.stringify('foo \u2029 bar'))
		
		This is of course OK though:
		JSON.parse(JSON.stringify('foo \u2029 bar'))
		
		So we need to escape these two characters if we want to *evaluate* the
		resulting JSON. The problematic characters in JSON can occur only
		inside (JSON) strings. That is why the following replacement works:
		
		'["<U+2028>"]' -> '["\u2028"]'
		
		This is not possible (not a JSON, never created by JSON.stringify)
		according to RFC 7159.
		'[""]<U+2028>' -> '[""]\u2028'
		
	*/
	const funcArgs = Array.map(args, _toJSON).join(', ')
		.split('\u2028').join('\\u2028')
		.split('\u2029').join('\\u2029');
	
	
	/*
		Minimize accessibility to addon-specific objects
		It is always a good idea to restrict access to internal APIs
	*/
	const protectedVariables = [
		'self'
		, 'port'
		, 'postMessage'
		, 'on'
		, 'once'
		, 'removeListener'
		, 'options'
		, 'unsafeWindow'
		, 'cloneInto'
		, 'exportFunction'
		, 'importFunction'
		, 'createObjectIn'
		, 'XPCNativeWrapper'
		, 'isProxy'
		, 'debug'
		
		// deprecated
		, 'Components'
		, 'StopIteration'
	];
	const wrapperArgs = protectedVariables.join(', ');
	
	const replacedVariables = ['window', 'self'];
	
	const restrictedVariables = protectedVariables.filter(value => -1 == replacedVariables.indexOf(value));
	
	
	const restrictedJSON = _toJSON();
	
	
	/*
		Note: This script is carefully composed, be careful!
		
		ES6 Proxy is used here.
		`console` is customized to display the date.
		`window` is proxied to prevent access to window.self
		
		The result of the script is sent with a 'scriptReturn' event
		
		The error message (if any) serialized into a string is sent with a
		'scriptError' event
		
		TODO: Refactor to make it clear what this code does and ease reviewing
		
		Reflect is available in Firefox >= 42
		TODO: Once 45ESR has widely replaced 38ESR, remove the polyfill
		
		This is a ES6 Template String:
		
		https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/template_strings
		
		
		These object do expose internal APIs:
		
		window, window.window
		parent, window.parent === window
		top, window.top === window
		
		
		These do not:
		
		frames, window.frames !== window
		window.frames.parent !== window
		window.frames.top !== window
		window.frames.window !== window
		
	*/
	const script = `
	try {
		const result = (function (${wrapperArgs}) {
			const parent = self;
			const top = self;
			const frames = self;
			return (function (window, console) {
				return (${funcStr})(${funcArgs});
			}(self, new Proxy(Object.create(null), {get (o, i) {
				if ("function" == typeof console[i]) {
					return console[i].bind(console, "[" + (new Date).toISOString() + "]");
				}
				return function () {};
			}})))
		}(function () {
			const isAccessible = name =>
				-1 == ${_toJSON(restrictedVariables)}.indexOf(name);
			
			// Reflect.ownKeys() for Firefox < 42
			const reflectOwnKeys = obj =>
				Object.getOwnPropertyNames(obj)
				.concat(Object.getOwnPropertySymbols(obj));
			// Object.getOwnPropertySymbols(): available from Firefox 36
			
			return new Proxy(Object.create(null), {
				get (o, i, receiver) {
					if ("self" == i) {
						return receiver;
					}
					if ("window" == i) {
						return receiver;
					}
					if ("top" == i) {
						return receiver;
					}
					if ("parent" == i) {
						return receiver;
					}
					if ("frames" == i) {
						return receiver;
					}
					if (!isAccessible(i)) {
						return o[i];
					}
					return window[i];
				},
				set (o, i, value, receiver) {
					if (!isAccessible(i)) {
						o[i] = value;
					} else {
						window[i] = value;
					}
					return true;
				},
				enumerate (o) {
					return function* () {
						for (let i in window) {
							if (!isAccessible(i)) continue;
							yield i;
						}
						for (let i in o) {
							yield i;
						}
					}();
				},
				has (o, i) {
					if (!isAccessible(i)) {
						return i in o;
					}
					return i in window;
				},
				deleteProperty (o, i) {
					if (!isAccessible(i)) {
						return delete o[i];
					}
					return delete window[i];
				},
				ownKeys (o) {
					return reflectOwnKeys(window).filter(isAccessible)
						.concat(reflectOwnKeys(o));
				},
				defineProperty (o, i, desc) {
					if (!isAccessible(i)) {
						return Object.defineProperty(o, i, desc);
					}
					return Object.defineProperty(window, i, desc);
				}
			});
		}()));
		self.port.emit("scriptReturn", result);
	} catch (e) {
		self.port.emit("scriptError", e + "");
	}
	`;
	
	return script;
};


/* Exports */
exports.buildScript = buildScript;


// vim: ts=4 noet ai

